/*
 * FH wdt subsystem
 *
 * Copyright (C) 2014 Fullhan Microelectronics Co., Ltd.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */
#define pr_fmt(fmt) "fh_wdt: " fmt

#include <linux/bitops.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/pm.h>
#include <linux/platform_device.h>
#include <linux/spinlock.h>
#include <linux/timer.h>
#include <linux/uaccess.h>
#include <linux/watchdog.h>
#include <linux/interrupt.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_platform.h>
#include <mach/pmu.h>

#define WDT_DEFAULT_TIMEOUT		10
#define WDT_MIN_TIMEOUT			1

#define WDOG_LOAD_VALUE_OFFSET			0x00
#define WDOG_CTRL_OFFSET				0x04
#define WDOG_INT_CLEAR_OFFSET			0x08
#define WDOG_RAW_INT_OFFSET				0x0C
#define WDOG_INT_STATUS_OFFSET			0x10
#define WDOG_CNT_VALUE_OFFSET			0x14
#define WDOG_OP_LOCK_OFFSET				0x18
#define WDOG_CNT_READ_OFFSET			0x1C
#define WDOG_IRQ_VALUE_OFFSET			0x20
#define WDOG_UNLOCK_KEY					(0x1ACCE551)

/*
 * WDT_CTRL Register
 */
#define WDOG_IRQ_EN						BIT(0)
#define WDOG_RUN						BIT(1)
#define WDOG_NEW						BIT(2)
#define WDOG_RST_EN						BIT(3)
#define WDOG_WAIT_SYNC					BIT(4)
#define WDT_CLOCK						32768

static int nowayout = WATCHDOG_NOWAYOUT;
module_param(nowayout, int, 0);
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started "
		"(default=" __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");

static int wdt_timeout = WDT_DEFAULT_TIMEOUT;
module_param(wdt_timeout, int, 0);
MODULE_PARM_DESC(wdt_timeout,
		"Watchdog time in seconds. (default="
		__MODULE_STRING(CDNS_WDT_DEFAULT_TIMEOUT) ")");

struct fh_wdt {
	spinlock_t			lock;
	bool				rst;
	void __iomem		*regs;
	struct clk			*clk;
	struct watchdog_device wdd;
};

static void wdt_wait_sync(struct fh_wdt *wdt)
{
	u32 reg;

	do
	{
		reg = readl_relaxed(wdt->regs + WDOG_RAW_INT_OFFSET);
	} while (reg & WDOG_WAIT_SYNC);
}

static void wdt_write_reg(struct fh_wdt *wdt, u32 offset, u32 val, u32 mask)
{
	u32 reg = readl_relaxed(wdt->regs + offset);
	reg &= ~mask;
	reg |= val;
	writel_relaxed(WDOG_UNLOCK_KEY, wdt->regs + WDOG_OP_LOCK_OFFSET);
	writel_relaxed(reg, wdt->regs + offset);
	writel_relaxed(0, wdt->regs + WDOG_OP_LOCK_OFFSET);
}

static int wdt_stop(struct watchdog_device *wdd)
{
	struct fh_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags;

	spin_lock_irqsave(&wdt->lock, flags);
	wdt_write_reg(wdt, WDOG_CTRL_OFFSET, 0, WDOG_RUN);
	spin_unlock_irqrestore(&wdt->lock, flags);

	return 0;
}

static int wdt_reload(struct watchdog_device *wdd)
{
	struct fh_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags;

	spin_lock_irqsave(&wdt->lock, flags);
	wdt_write_reg(wdt, WDOG_LOAD_VALUE_OFFSET, wdd->timeout * WDT_CLOCK, 0xFFFFFFFF);
	spin_unlock_irqrestore(&wdt->lock, flags);

	return 0;
}

static int wdt_start(struct watchdog_device *wdd)
{
	struct fh_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags, load_count, data;

	data        = WDOG_RUN | WDOG_NEW;
	load_count  = wdd->timeout * WDT_CLOCK;

	//default use reset mode, in case system cannot reset when
	//system cannot response interrupt(such as system was crashed).
	data |= WDOG_RST_EN;
	data &= ~WDOG_IRQ_EN;
	// if (wdt->rst) {
	//	data |= WDOG_RST_EN;
	//	data &= ~WDOG_IRQ_EN;
	// } else {
	//	data &= ~WDOG_RST_EN;
	//	data |= WDOG_IRQ_EN;
	// }

	spin_lock_irqsave(&wdt->lock, flags);
	wdt_write_reg(wdt, WDOG_CTRL_OFFSET, data, 0xFFFFFFFF);
	wdt_write_reg(wdt, WDOG_LOAD_VALUE_OFFSET, load_count, 0xFFFFFFFF);
	wdt_write_reg(wdt, WDOG_INT_CLEAR_OFFSET, 0x1, 0x1);
	wdt_write_reg(wdt, WDOG_IRQ_VALUE_OFFSET, 0, 0xFFFFFFFF);
	set_bit(WDOG_HW_RUNNING, &wdd->status);
	wdt_wait_sync(wdt);
	spin_unlock_irqrestore(&wdt->lock, flags);

	return 0;
}

static int wdt_settimeout(struct watchdog_device *wdd, unsigned int new_time)
{
	wdd->timeout = new_time;
	return wdt_start(wdd);
}

static unsigned int wdt_gettimeout(struct watchdog_device *wdd)
{
	int time_left;
	struct fh_wdt *wdt = watchdog_get_drvdata(wdd);

	time_left = readl_relaxed(wdt->regs + WDOG_CNT_READ_OFFSET);
	return time_left / WDT_CLOCK;
}

static irqreturn_t wdt_interrupt(int this_irq, void *dev_id)
{
	struct platform_device *pdev = dev_id;
	struct fh_wdt *wdt = platform_get_drvdata(pdev);

	wdt_stop(&wdt->wdd);
	wdt_write_reg(wdt, WDOG_INT_CLEAR_OFFSET, 0x1, 0x1);
	wdt->rst = 1;
	wdt_settimeout(&wdt->wdd, 1);

	return IRQ_HANDLED;
}

#ifdef CONFIG_PM
static int wdt_mol_runtime_suspend(struct device *dev)
{
	return 0;
}

static int wdt_mol_runtime_resume(struct device *dev)
{
	return 0;
}

UNIVERSAL_DEV_PM_OPS(wdt_mol_pm_ops, wdt_mol_runtime_suspend,
		   wdt_mol_runtime_resume, NULL);

#define WDT_MOL_PM_OPS (&wdt_mol_pm_ops)
#else
#define WDT_MOL_PM_OPS NULL
#endif /* CONFIG_PM */

static const struct watchdog_info fh_wdt_ident = {
	.options	= WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT |
			WDIOF_MAGICCLOSE,
	.identity	= " Fullhan Watchdog",
};

static const struct watchdog_ops fh_wdt_ops = {
	.owner = THIS_MODULE,
	.start = wdt_start,
	.stop  = wdt_stop,
	.ping  = wdt_reload,
	.get_timeleft = wdt_gettimeout,
	.set_timeout  = wdt_settimeout,
};

#ifdef CONFIG_USE_OF
static struct of_device_id const fh_wdt_of_match[] = {
	{
		.compatible = "fh,fh-wdtv2",
	},
	{}
};
MODULE_DEVICE_TABLE(of, fh_wdt_of_match);
#endif


static int fh_wdtv2_drv_probe(struct platform_device *pdev)
{
	int ret, irq;
#ifdef CONFIG_USE_OF
	const struct of_device_id *match;
	struct device_node *np = pdev->dev.of_node;
#else
	struct resource *res;
#endif
	struct fh_wdt *wdt;
	struct watchdog_device *wdt_device;

	wdt = devm_kzalloc(&pdev->dev, sizeof(*wdt), GFP_KERNEL);
	if (!wdt)
		return -ENOMEM;

	wdt_device = &wdt->wdd;
	wdt_device->info = &fh_wdt_ident;
	wdt_device->ops  = &fh_wdt_ops;
	wdt_device->timeout     = WDT_DEFAULT_TIMEOUT;
	wdt_device->min_timeout = WDT_MIN_TIMEOUT;
	wdt_device->max_timeout = UINT_MAX / WDT_CLOCK;
	wdt_device->parent      = &pdev->dev;

	wdt->clk = clk_get(NULL, "wdt_clk");

	if (IS_ERR(wdt->clk))
		return -ENODEV;

	clk_prepare_enable(wdt->clk);

#ifdef CONFIG_USE_OF
	match = of_match_device(fh_wdt_of_match, &pdev->dev);
	if (!match) {
		pr_info("failed to find wdt!\n");
		return -ENODEV;
	}

	wdt->regs = of_iomap(np, 0);
	if (wdt->regs == NULL) {
		ret = -ENOMEM;
		return ret;
	}

	irq = irq_of_parse_and_map(np, 0);
	if (irq < 0) {
		dev_err(&pdev->dev, "no irq resource\n");
		ret = -ENXIO;
		goto release_region;
	}
#else
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		dev_err(&pdev->dev, "can't fetch device resource info\n");
		return -EIO;
	}

	wdt->regs = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(wdt->regs))
		return PTR_ERR(wdt->regs);

	irq = irq_create_mapping(NULL, platform_get_irq(pdev, 0));
	if (irq < 0) {
		dev_err(&pdev->dev, "no irq resource\n");
		goto release_region;
	}
#endif
	ret = request_irq(irq, wdt_interrupt, IRQF_NO_THREAD, "wdt", pdev);
	if (ret) {
		dev_err(&pdev->dev, "failure requesting irq %i\n", irq);
		goto release_region;
	}

	ret = watchdog_register_device(wdt_device);
	if (ret) {
		dev_err(&pdev->dev, "Failed to register wdt device\n");
		goto release_region;
	}
	platform_set_drvdata(pdev, wdt);
	spin_lock_init(&wdt->lock);
	watchdog_set_drvdata(wdt_device, wdt);

	return 0;

release_region:
	iounmap(wdt->regs);
	return ret;
}

static int fh_wdtv2_drv_remove(struct platform_device *pdev)
{
	struct fh_wdt *wdt = platform_get_drvdata(pdev);

	wdt_stop(&wdt->wdd);

	if (!IS_ERR(wdt->clk)) {
		clk_disable_unprepare(wdt->clk);
		clk_put(wdt->clk);
	}

	watchdog_unregister_device(&wdt->wdd);

	return 0;
}


static struct platform_driver fh_wdt_driver = {
	.probe		= fh_wdtv2_drv_probe,
	.remove		= fh_wdtv2_drv_remove,
	.driver		= {
		.name	= "fh_wdtv2",
		.owner	= THIS_MODULE,
		.pm	= WDT_MOL_PM_OPS,
#ifdef CONFIG_USE_OF
		.of_match_table = fh_wdt_of_match,
#endif
	},
};

static int __init fh_wdt_watchdog_init(void)
{
	return platform_driver_register(&fh_wdt_driver);
}
module_init(fh_wdt_watchdog_init);

static void __exit fh_wdt_watchdog_exit(void)
{
	platform_driver_unregister(&fh_wdt_driver);
}
module_exit(fh_wdt_watchdog_exit);

MODULE_AUTHOR("fullhan");
MODULE_DESCRIPTION("Fullhan WatchDog device driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR);
